﻿//Copyright (c) 2010 Christopher E. S. King (http://chriskingconsulting.com)
//
//Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files 
// (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, 
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do 
// so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE 
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.IO;
using System.Xml.Linq;
using System.Collections;

using SysEnv = System.Environment;

using King.Extensions;
using King.Automation;
using King.Text;
using King.Reflection;
using King.Declarative;
using System.Xml;

namespace King.TeamFoundation {

    public class Program {

        public static int Main(string[] args) {
            return ComandConsoleApplication.EntryPoint(args);
        }
    }

    public struct TfsContext {

        public static implicit operator TfsContext(DeclarationContext context) {
            return new TfsContext(context);
        }

        private DeclarationContext m_context;

        internal TfsContext(DeclarationContext context) {
            m_context = context;
        }

        public TfsLoader Loader {
            get { return m_context.Ancestors<TfsRoot>().Single().Loader; }
        }
        public TfsCollection Collection {
            get { return Loader.Collection; }
        }
        public TfsFolder LabFolder {
            get { return Collection.LabFolder; }
        }
        public TfsWorkspace Workspace {
            get { return Loader.Workspace; }
        }
        public TextPath LabDir {
            get { return new TextPath(isDirectory: true) { LabFolder.Path }; }
        }
    }

    [Declaration(UseDedicatedStack = true)]
    internal sealed class TfsRoot : Declaration {

        private TfsLoader m_loader;
        private string m_path;

        internal TfsRoot(string path) {
            var vsCommonTools = SysEnv.GetEnvironmentVariable("VS100COMNTOOLS");
            if (vsCommonTools != null) {
                var vsCommonToolsUrl = new Uri(vsCommonTools);
                var devEnvDir = new Uri(vsCommonToolsUrl, @"..\IDE");
                SysEnv.SetEnvironmentVariable("PATH", SysEnv.GetEnvironmentVariable("PATH") + ";" + devEnvDir.LocalPath);
            }

            m_path = path;
        }

        internal TfsLoader Loader {
            get {
                if (m_loader == null)
                    m_loader = new TfsLoader(Path.GetDirectoryName(m_path));
                return m_loader;
            }
        }
    }

    [Declaration(AdaptorType = typeof(Adaptor))]
    public abstract class TfsCommand : Command {

        protected new class Adaptor : GenericDeclaration.Adaptor {

            public Adaptor(Type adaptedType)
                : base(adaptedType) {
            }

            public override object Prolog(DeclarationContext frame, object value) {

                if (!(frame.IsEvaluating))
                    return null;

                if (value is TfsRoot)
                    return null;

                if (frame.Ancestors(typeof(TfsRoot)).Any())
                    return null;

                var command = (TfsCommand)value;
                return new TfsRoot(command.m_path) { value };
            }
        }

        private string m_path;
        private TfsLoader m_loader;

        internal TfsCommand(params object[] arguments)
            : base(arguments) {
            var vsCommonTools = SysEnv.GetEnvironmentVariable("VS100COMNTOOLS");
            if (vsCommonTools != null) {
                var vsCommonToolsUrl = new Uri(vsCommonTools);
                var devEnvDir = new Uri(vsCommonToolsUrl, @"..\IDE");
                SysEnv.SetEnvironmentVariable("PATH", SysEnv.GetEnvironmentVariable("PATH") + ";" + devEnvDir.LocalPath);
            }

            m_path = (string)arguments[0];
        }

        protected TfsLoader Loader {
            get { throw new NotSupportedException(); } 
        }
        protected TfsCollection Collection { 
            get { return Loader.Collection; } 
        }
        protected TfsFolder LabFolder {
            get { return Collection.LabFolder; } 
        }
        protected TfsWorkspace Workspace {
            get { return Loader.Workspace; } 
        }
        protected TfsFolder CurrentFolder { 
            get { return Workspace.CurrentFolder.GetFolder(m_path); } 
        }
        protected TextPath LabDir {
            get { return new TextPath(isDirectory: true) { LabFolder.Path }; }
        }
    }

    /// <summary>
    /// Commands to convert solution files to and from XML to simplify rewriting and
    /// to binplace binaries generated by building a solution file.
    /// </summary>
    [CommandGroup]
    public abstract class Solution : TfsCommand {

        internal Solution(params object[] arguments)
            : base(arguments) {
        }
    }
    public sealed class SolutionToXml : Solution {

        private string m_itemspec;

        [Command]
        internal SolutionToXml(
            [CommandTarget]string itemspec)
            : base(itemspec) {
        }

        private IEnumerable Execute(string itemspec) {
            var info = TfsLoader.GetInfo(typeof(TfsSolution));
            var xml = info.ToXml(m_itemspec);
            return xml.ToString();
        }
    }
    public sealed class SolutionFromXml : Solution {

        private string m_itemspec;

        [Command]
        internal SolutionFromXml(
            [CommandTarget]string itemspec)
            : base(itemspec) {
        }

        private IEnumerable Execute(string itemspec) {
            var info = TfsLoader.GetInfo(typeof(TfsSolution));
            var sln = info.FromXml(XElement.Load(m_itemspec));
            yield return sln;
        }
    }
    public sealed class SolutionBinplace : Solution {

        private abstract class BinplaceProjectConfiguration : TextDeclaration {

            internal static BinplaceProjectConfiguration Activate(
                BinplaceSolutionConfiguration solution, TfsProjectConfiguration configuration) {

                var csConfiguration = configuration as TfsCsProjectConfiguration;
                if (csConfiguration != null) {

                    var csProject = csConfiguration.Project;

                    // hacks
                    if (csProject.NameWithoutExtension.ToLower() == "test") return null;

                    // C# web project
                    if (csProject.IsWebProject)
                        return new BinplaceCSharpWebConfiguraiton(solution, csConfiguration);

                    // C# exe
                    if (csProject.IsExe)
                        return new BinplaceCSharpExeConfiguraiton(solution, csConfiguration);
                }

                var vcxConfiguraiton = configuration as TfsVcxProjectConfiguration;
                if (vcxConfiguraiton != null) {

                    // C++ exe
                    if (vcxConfiguraiton.IsApplication)
                        return new BinplaceCppExeConfiguraiton(solution, vcxConfiguraiton);
                }

                return null;
            }

            protected readonly TfsProjectConfiguration m_configuration;
            protected readonly BinplaceSolutionConfiguration m_solution;

            public BinplaceProjectConfiguration(BinplaceSolutionConfiguration solution, TfsProjectConfiguration configuration) {
                m_configuration = configuration;
                m_solution = solution;
            }

            protected string Target {
                get { return m_solution.Target; }
            }
            protected string MirrorDir {
                get { return m_solution.MirrorDir; }
            }
            protected TfsSolutionConfiguration SolutionConfiguration {
                get { return m_solution.SolutionConfiguration; }
            }
            protected TfsProjectConfiguration Configuration {
                get { return m_configuration; }
            }
            protected virtual IEnumerable Files() {
                var project = Configuration.Project;

                return new object[] {

                    // binplace binary
                    new TextPath { m_solution.Directory, Configuration.BinaryPath }, TargetDir,

                    // binplace symbols
                    new TextPath { m_solution.Directory, Configuration.SymbolPath }, TargetDir,

                    // binplace referenced projects
                    from o in project.ProjectReferences()
                    let reference = SolutionConfiguration.GetProjectConfiguration(o)
                    select new object[] { 

                        // referenced binary
                        reference.BinaryPath, TargetDir,

                        // referenced binary symbols
                        reference.SymbolPath, TargetDir,
                    },

                    // binplace referenced binaries (not compiled locally)
                    from o in project.FileReferences()
                    select new object[] {
                        o.Path, TargetDir
                    },
                };
            }
            protected abstract string TargetDir { get; }

            protected override IEnumerable Children(DeclarationContext frame) {
                var project = Configuration.Project;
                var sourceDir = project.Directory;
                var targetDir = new TextPath(isDirectory: true) { Target, project.NameWithoutExtension };
                var files = new TextFlatten { Files() };

                return new TextHeader(
                    new ConsoleMarkup(ConsoleColor.Cyan) { 
                        new TextJoin(" ") { project.Name, 
                            new TextJoin("|", "(", ")") { Configuration.Flavor, Configuration.Platform },
                        }
                    })
                {
                    new Binplace(
                        sourceDir: sourceDir, 
                        cacheDir: MirrorDir, 
                        targetDir: targetDir,
                        paths: files.ToArray()
                    )
                };
            }
        }
        private abstract class BinplaceCSharpConfiguraiton : BinplaceProjectConfiguration {

            public BinplaceCSharpConfiguraiton(BinplaceSolutionConfiguration solution, TfsCsProjectConfiguration configuration)
                : base(solution, configuration) {
            }

            protected new TfsCsProjectConfiguration Configuration {
                get { return (TfsCsProjectConfiguration)base.Configuration; }
            }
        }
        private sealed class BinplaceCSharpWebConfiguraiton : BinplaceCSharpConfiguraiton {

            public BinplaceCSharpWebConfiguraiton(BinplaceSolutionConfiguration solution, TfsCsProjectConfiguration configuration)
                : base(solution, configuration) {
            }

            protected override string TargetDir {
                get { return new TextPath(isDirectory: true) { "bin" }; }
            }
            protected override IEnumerable Files() {
                var project = Configuration.Project;

                // common binary files
                yield return base.Files();

                // binplace content
                yield return
                    from o in project.Content()
                    let path = o.Item.Path
                    let pathRelativeToProject =
                        project.DirectoryUrl.MakeRelativePath(o.ProjectPathUrl)
                    select new object[] { 
                        path, pathRelativeToProject 
                    };
            }
        }
        private sealed class BinplaceCSharpExeConfiguraiton : BinplaceCSharpConfiguraiton {

            public BinplaceCSharpExeConfiguraiton(BinplaceSolutionConfiguration solution, TfsCsProjectConfiguration configuration)
                : base(solution, configuration) {
            }
            protected override string TargetDir {
                get { return new TextPath(isDirectory: true) { "." }; }
            }
        }
        private sealed class BinplaceCppExeConfiguraiton : BinplaceProjectConfiguration {

            public BinplaceCppExeConfiguraiton(BinplaceSolutionConfiguration solution, TfsVcxProjectConfiguration configuration)
                : base(solution, configuration) {
            }
            protected override string TargetDir {
                get { return new TextPath(isDirectory: true) { "." }; }
            }
        }

        private class BinplaceSolutionConfiguration : TextDeclaration {

            private readonly TfsContext m_context;
            private readonly string m_itemspec;
            private readonly string m_target;
            private readonly string m_configuration;
            private readonly string m_platform;
            private readonly IEnumerable<string> m_projects;
            private readonly string m_mirrorDir;

            private TfsSolution m_solution;
            private TfsSolutionConfiguration m_solutionConfiguration;

            internal BinplaceSolutionConfiguration(TfsContext context,
                string itemspec, TextPath target, string configuration, string platform, IEnumerable<string> projects) {

                m_context = context;
                m_itemspec = itemspec;
                m_target = target;
                m_configuration = configuration;
                m_platform = platform;
                m_projects = projects;

                m_mirrorDir = m_target + "build\\";
            }

            private IEnumerable Initialize() {
                yield return new ConsoleFlush { "Loading project graph..." };
                m_context.LabFolder.LoadProjectGraph();

                // get solution configuration
                m_solution = m_context.Collection.Get<TfsSolution>(m_itemspec);
                m_solutionConfiguration = m_solution.GetSolutionConfiguration(
                    TfsConfiguration.Get(
                        TfsConfiguration.ParseFlavor(m_configuration),
                        TfsConfiguration.ParsePlatform(m_platform)
                    )
                );
            }
            private IEnumerable<TfsProjectConfiguration> Configurations() {
                var configurations =
                    from configuration in m_solutionConfiguration.ProjectConfigurations()
                    where configuration.GeneratesBinary
                    select configuration;

                foreach (var configuration in configurations)
                    yield return configuration;
            }

            public string Target {
                get { return m_target; }
            }
            public string MirrorDir {
                get { return m_mirrorDir; }
            }
            public TfsSolutionConfiguration SolutionConfiguration {
                get { return m_solutionConfiguration; }
            }
            public object Directory {
                get { return Path.GetDirectoryName(m_itemspec).NormalizeDirectory(); }
            }

            protected override IEnumerable Children(DeclarationContext frame) {

                return new TextSingleSpaced {

                    // report root directory into which build files to deploy are mirrored
                    new ConsoleWhite { new TextJoin(" ") { "Target directory: ", m_mirrorDir } },

                    // cache project graph
                    new SubstantialDeclaration { Initialize() },

                    // binplace projects
                    new SubstantialDeclaration { 
                        new TextSingleSpaced {
                            from projectConfiguration in Configurations()
                            select BinplaceProjectConfiguration.Activate(this, projectConfiguration)
                        }
                    }
                };
            }

        }

        [Command]
        internal SolutionBinplace(
            string itemspec,
            [CommandTarget(Path = CommandPathInfo.IsDirectory)]string target = null,
            [CommandSwitch("buildFlavor")]string flavor = "debug",
            [CommandSwitch("buildPlatform")]string platform = "Mixed Platforms",
            [CommandTarget("project")]params string[] projects)
            : base(itemspec, target, flavor, platform, projects) {
        }

        private IEnumerable Execute(TfsContext context, 
            string itemspec, TextPath target, string flavor, string platform, string[] projects) {

            return new TextHeader(new ConsoleCyan {
                new TextJoin(" ") {
                    "Binplace solution", itemspec, new TextJoin("|", "(", ")") { flavor, platform }
                }
            }) { 
                new BinplaceSolutionConfiguration(context, itemspec, target, flavor, platform, projects) 
            };

            #region C# Binary References
            //// verify referenced files exist
            //var missingPublicFiles =
            //    from project in csProjects
            //    from file in project.FileReferences()
            //    where !File.Exists(file.Path)
            //    group file.Path ?? file.Name by project;
            //if (missingPublicFiles.Any()) {

            //    yield return new ConsoleMarkup(ConsoleColor.Red) {
            //        new TextHeader("ERROR: Referenced files not found:") {
            //            from o in missingPublicFiles 
            //            select new TextHeader(o.Key.Path + ":", Substantial) { o }
            //        }
            //    };
            //    yield break;
            //}

            //// filter out duplicate references
            //var publicFiles = csProjects.SelectMany(o => o.FileReferences()).Distinct();

            // copy references
            //yield return new TextHeader("Copy referenced binaries:", nl) {
            //    new Copy(LabFolder.Path, targetBuildDir, 
            //        relative: true, 
            //        sourceTarget: publicFiles.Select(o => o.Path).ToArray())
            //};

            //#region C# Generated Files
            //// copy genereated dlls
            //yield return new TextHeader("Copy generated binaries:", nl) {
            //    new Copy(LabFolder.Path, targetBuildDir, 
            //       relative: true,
            //       sourceTarget: (from o in csProjectConfigurations
            //        where !string.IsNullOrEmpty(o.BinaryPath)
            //        select o.BinaryPath).ToArray())
            //};
            //#endregion

            //#region C# Web Project Content
            //var webFiles =
            //    from project in csProjects
            //    where project.IsWebProject
            //    from file in project.Content()
            //    select new { Project = project, Item = file.Item };

            //// verify referenced files exist
            //var missingWebFiles =
            //    from file in webFiles
            //    where !File.Exists(file.Item.Path)
            //    group file.Item.Path ?? file.Item.Name by file.Project;
            //if (missingWebFiles.Any()) {

            //    yield return new ConsoleMarkup(ConsoleColor.Red) {
            //        new TextHeader("ERROR: Referenced web files not found:") {
            //            from o in missingWebFiles 
            //            select new TextHeader(o.Key.Path + ":", Substantial) { o }
            //        }
            //    };
            //    yield break;
            //}

            //// filter out duplicate references
            //var distinctWebFiles = webFiles.Select(o => o.Item).Distinct();

            //// copy references
            //yield return new TextHeader("Copy web files binaries:", nl) {
            //    new Copy(LabFolder.Path, targetBuildDir, 
            //        relative: true,
            //        sourceTarget: webFiles.Select(o => o.Item.Path).ToArray())
            //};
            //#endregion

            //yield return new TextHeader("Web Projects:", nl) {
            //    from o in csProjectConfigurations
            //    let project = o.Project
            //    where project.IsWebProject
            //    select new TextHeader(project.Path, Substantial) {
            //        from f in project.Content()
            //        orderby f.ProjectPath
            //        select new TextJoin(" ") {
            //            f.ProjectPath, !f.IsLink ? null :
            //                "(" + f.Item.Path + ")"
            //        }
            //    }
            //};

            //foreach (var projectConfiguration in solutionConfiguration.ProjectConfigurations(true)) {
            //yield return new TextSingleSpaced {
            //    from projectConfiguration in projectConfigurations
            //    let project = projectConfiguration.Project
            //    let IsWebProject = project is TfsCsProject && ((TfsCsProject)project).IsWebProject
            //    let OutputType = project is TfsCsProject ? ((TfsCsProject)project).OutputType : null
            //    orderby project.Path
            //    group project by new { 
            //        project.Extension, 
            //        IsWebProject,
            //        OutputType
            //    } into projectGroups
            //    orderby projectGroups.Key.Extension
            //    select new TextSingleSpaced {
            //        projectGroups.Key,
            //        new TextIndent {
            //            new TextSingleSpaced {
            //                projectGroups.Select(o => o.Path)
            //            }
            //        }
            //    }
            //};         
            #endregion
        }
    }
    public sealed class SolutionGenerate : Solution {

        /// <summary>
        /// Generate a solution of containing all projects in all sub-directories. 
        /// </summary>
        /// <param name="itemspec">The name of the solution to generated. Relative paths are resolved from the working directory.</param>
        /// <param name="version">The version of Visual Studio to target. Must be 2010 or 2008.</param>
        /// <param name="unmanagedPlatform">The platform version of unmanaged projects to target. Must be x64 or x86.</param>
        /// <param name="exclude">Patterns of solution and\or project names to exclude. 
        /// For example, "_*.sln" will exclude all solution files starting with underscore.</param>
        /// <param name="include">Patterns of project names to include. If not specified then all solutions and known
        /// project types are included. For example, "*.sln;*.csproj" will exclude all solutions
        /// and C# projects.</param>
        /// <param name="verify">Verifies that the generated file matches the existing file.</param>
        [Command]
        public SolutionGenerate(
            [CommandTarget(DefaultValue = "master.sln")]
            string itemspec, 

            int version = 2010, 
            string unmanagedPlatform = "x64",
            string[] exclude = null,
            string[] include = null,
            bool verify = false)
            : base(itemspec, version, unmanagedPlatform, exclude, include, verify) {
        }

        private IEnumerable Execute(string itemspec, int version, string unmanagedPlatform,
            string[] exclude, string[] include, bool verify) {

            LabFolder.LoadProjectGraph();

            var solutions = LabFolder.Get<TfsSolution>();

            // create a new solution
            var newSolution = CurrentFolder.NewSolution(itemspec, version, unmanagedPlatform);

            // find all projects in subfolders of the working folder
            var projects =
               (from project in CurrentFolder.Get<TfsProject>()
                where project.Guid != null
                where IncludeItem(project.Name, exclude, include)
                orderby project.Url.AbsoluteUri
                select project).ToArray();

            // create solution folders to mirror actual folders
            var folders = projects.SelectMany(project => project.Folder.ToLinkList(o => o.Parent, o => o != CurrentFolder)).Distinct();
            var subFoldersByFolder = folders.GroupBy(o => o.Parent).ToDictionary(o => o.Key);
            var solutionFoldersByFolder = folders.ToDictionary(o => o, o => newSolution.AddSolutionFolder(o.Name, o.ServerPath));
            foreach (var pair in solutionFoldersByFolder.OrderBy(o => o.Key.Path)) {
                var folder = pair.Key;
                var solutionFolder = pair.Value;

                var subFolders = subFoldersByFolder.GetValueOrDefault(folder);
                if (subFolders != null) {
                    foreach (var subFolder in subFolders)
                        solutionFolder.AddSubFolder(solutionFoldersByFolder[subFolder]);
                }
            }

            // add projects to folders
            foreach (var project in projects)
                solutionFoldersByFolder[project.Folder].AddItem(project);

            // find dependencies for a given project
            var dependencies =
               (from solution in CurrentFolder.Get<TfsSolution>()
                where IncludeItem(solution.Name, exclude, include)
                from o in solution.Dependencies()
                where IncludeItem(o.Key.Name, exclude, include)
                from dependency in o
                where IncludeItem(dependency.Name, exclude, include)
                orderby dependency.GetGuid(solution)
                group dependency by o.Key).ToArray();

            // register dependecies
            foreach (var dependeciesForProject in dependencies) {
                var project = dependeciesForProject.Key as TfsProject;
                if (project == null || project.Guid == null)
                    continue;

                foreach (var dependency in dependeciesForProject.Distinct())
                    newSolution.AddDependency(project, dependency);
            }

            if (verify) {
                var oldSolution = Collection.Get<TfsSolution>(itemspec);
                if (oldSolution == null)
                    throw new ArgumentException("No solution file exists at " + itemspec);
                var oldSolutionFile = oldSolution.ReadToEnd();
                var newSolutionFile = newSolution.ToSolutionFile();
                if (oldSolutionFile != newSolutionFile) {
                    var temp = Path.GetTempFileName();
                    File.WriteAllText(temp, newSolutionFile);
                    throw new ArgumentException("The new solution file would be different than the old one. See " + temp);
                }

            } else {
                Collection.WriteChanges();
            }

            yield break;
        }

        private bool IncludeItem(string name, string[] exclude, string[] include) {
            // compose regex for testing solution and project names for exclusion
            var excludeRegex = new Regex(exclude.Select(o => "^" + o.Replace("*", ".*") + "$").StringJoin("|") ?? "$^");
            var includeRegex = new Regex(include.Select(o => "^" + o.Replace("*", ".*") + "$").StringJoin("|") ?? ".");

            return includeRegex.IsMatch(name) && !excludeRegex.IsMatch(name);
        }
    }

    public enum LinkType {
        None,
        Symbolic,
        Hard
    }

    /// <summary>
    /// Copy files specified by a relative path under an absolute source root
    /// directory to an absolute target root directory. Optionally reconstruct
    /// the relative path under the target directory.
    /// </summary>
    [CommandGroup]
    public sealed class Binplace : TfsCommand {

        private enum CopyResult {
            Nominal,
            Cached,
        }

        [Command]
        public Binplace(
            [CommandSwitch(Path = CommandPathInfo.Exists | CommandPathInfo.IsDirectory)]
            string sourceDir = null,

            [CommandSwitch(Path = CommandPathInfo.IsDirectory)]
            string targetDir = null,

            [CommandSwitch(
                Path = CommandPathInfo.IsDirectory)
            ]string cacheDir = null,
            
            params string[] paths)
            : base(sourceDir, targetDir, cacheDir, paths) {
        }

        private CopyResult CopyFile(TextPath source, TextPath target) {
            var result = CopyResult.Nominal;

            // create the directory
            var targetDir = target.Directory;
            if (!Directory.Exists(targetDir))
                Directory.CreateDirectory(targetDir);

            // handle case where the file already exists
            var test = source.CompareAgeTo(target);
            if (test != TextPathAge.First) {

                // assume if the timestamps match the file is the same (ala msbuild)
                if (test != TextPathAge.Same)
                    throw new ArgumentException(
                        new TextHeader("Target file exists and has different time stamp than source file. ") {
                            "Source file created at " + File.GetLastWriteTime(source) + ": " + source,
                            "Target file created at " + File.GetLastWriteTime(target) + ": " + target
                        }.ReadToEnd()
                    );

                return CopyResult.Cached;
            }

            // copy file
            File.Copy(source, target, true);

            return result;
        }

        private IEnumerable CacheAndCopyFile(
            TextPath source,
            TextPath target,
            TextPath cache) {

            var sourceName = source.FileName;
            var targetName = target.FileName;

            // report the relative path to the root about to be copied
            yield return new ConsoleFlush() { 
                targetName.Equals(sourceName) ? (object)targetName :
                    new TextJoin(" ") { targetName, "<-", sourceName }
            };

            // cache file or report if file already cached
            if (cache != null) {
                var copyResult = CopyFile(source, cache);
                if (copyResult == CopyResult.Cached)
                    yield return new ConsoleYellow { " (cached)" };

                source = cache;
            }

            // copy file (from cache or source) to target
            CopyFile(source, target);
        }

        private Tuple<TextPath, TextPath> MakeFullyQualified(
            TextPath sourceRootDir,TextPath targetRootDir, Tuple<string, string> pair) {

            var source = new TextPath(isFile: true, mustExist: true) { sourceRootDir, pair.Item1 };
            var target = new TextPath(fileName: source.FileName) { targetRootDir, pair.Item2 };

            return Tuple.Create(source, target);
        }

        private IEnumerable Execute(
            TfsContext context, 
            TextPath sourceRootDir,
            TextPath targetRootDir,
            TextPath cacheRootDir, 
            IEnumerable<string> paths) {

            // report root source and target directories
            var header = new ConsoleMarkup(ConsoleColor.Cyan) {
                new TextJoin(" ") { 
                    "Binplace", targetRootDir, 
                    new TextJoin(prefix: "  [from: ", suffix: " ]") { sourceRootDir },
                }
            };

            var pairs = paths.ToPairs().ToArray();

            // nothing to binplace
            if (pairs.None())
                return new TextJoin(" ") { header, new ConsoleYellow { "(no files)" } };

            // test if first source file is the same age (or older) than the first target
            var first = MakeFullyQualified(sourceRootDir, targetRootDir, pairs.First());
            if (first.Item1.CompareAgeTo(first.Item2) <= 0)
                return new TextJoin(" ") { header, new ConsoleYellow { "(up to date)" } };

            return new TextHeader(header) {

                // report if and where files are being cached
                cacheRootDir == null ? null : new ConsoleMarkup(ConsoleColor.Yellow) { 
                    "Mirroring files at: " + cacheRootDir
                },

                // enumerate target/source file pairs
                from pair in pairs.Select(o => MakeFullyQualified(sourceRootDir, targetRootDir, o))
                let source = pair.Item1
                let target = pair.Item2

                // group by source and target directories
                group new { source, target } by new { 
                    SourceDir = source.Directory, 
                    TargetDir = target.Directory 
                } into grps 
                
                let sourceDir = grps.Key.SourceDir
                let targetDir = grps.Key.TargetDir
                let sourceRelDir = sourceRootDir.MakeRelative(sourceDir)
                let targetRelDir = targetRootDir.MakeRelative(targetDir)
                let matchingRelDirs = targetRelDir.Equals(sourceRelDir)
                
                // order by target/source directories
                orderby targetDir, !matchingRelDirs, sourceDir

                // target/source directory header
                select new TextHeader(new ConsoleMarkup(ConsoleColor.White) {
                    new TextJoin(" ") { 
                        
                        // normalize empty target dir to .\
                        string.IsNullOrEmpty(targetRelDir) ? (object)".\\" : targetRelDir, 

                        // if relative source and relative target directories match than display only target
                        matchingRelDirs ? null : new TextJoin(prefix: "  [from: ", suffix: " ]") { 

                            // if source dir is a sub directory of the root then disply only the relative source
                            sourceRootDir.Contains(sourceDir) ? sourceRelDir : sourceDir
                        } 
                    }
                }) {

                    // copy files
                    from grp in grps
                    orderby grp.target, grp.source

                    // mark as substantial so string joins do not probe into enumerator for substance testing
                    select new SubstantialDeclaration { 
                        CacheAndCopyFile(grp.source, grp.target, 
                        cacheRootDir == null ? null : 
                            new TextPath { cacheRootDir, context.LabDir.MakeRelative(grp.source) }) 
                    }
                }
            };
        }

    }

    //[CommandGroup]
    public abstract class Project : TfsCommand {

        internal Project(params object[] arguments)
            : base(arguments) {
        }
    }

    /// <summary>
    /// Rename the source to the target and fix project and file references. In the common case 
    /// where source is a directory and contains a single project whose name is (or is composed of) the 
    /// target name then that project will be renamed (or recomposed) using the target name in 
    /// addition to renaming the source directory. (e.g. Foo\Acme.Foo.csproj -> Bar\Acme.Bar.csproj)
    /// </summary>
    [CommandGroup]
    public class Rename : TfsCommand {
        
        /// <summary>
        /// 
        /// </summary>
        /// <param name="source">Source directory for the branch.</param>
        /// <param name="target">Target directory for the branch.</param>
        /// <param name="force">Force deletion of the source (move must be specified) even if other users 
        /// have checked out files in the source sub-tree.</param>
        /// <param name="noRename">If Source is a directory containing a single project in all it's sub-directories 
        /// then that project, it's AssemblyName, and RootNamespace will be renamed in addition to 
        /// branching the source directory unless this flag is set. </param>
        [Command]
        public Rename(
            [CommandTarget(Path = CommandPathInfo.Exists)]string source,
            [CommandTarget]string target, 
            bool force = false, 
            bool noRename = false)
            : base(source, target, force, noRename) {
        }

        private IEnumerable Execute(string source, string target, bool force, bool noRename) {
            var sourceFolder = Collection.GetFolder(source);
            var sourceUrl = Workspace.GetUrl(source);

            var status = Collection.Status();
            var openFilesInSource = status.Where(o => o.Path.Url.IsSubUrlOrEqual(sourceUrl));
            if (openFilesInSource.Any() && !force) {
                yield return new TextHeader("The following files are being manipulated in this or other workspaces: ") {
                    from o in openFilesInSource
                    let path = o.Path
                    from s in path.StatusElsewhere()
                    orderby path.Path
                    select s.ToString()
                };

                yield break;
            }

            var sourcePath = Collection.GetPath(source);
            if (sourcePath.IsFolder)
                target = target.NormalizeDirectory();

            // cache all projects and solutions
            LabFolder.LoadProjectGraph();

            // discover any projects in the source directory before they are deleted
            var projects = sourcePath.Get<TfsProject>();

            // execute rename
            sourcePath.Rename(target);

            if (sourcePath.IsFolder && projects.OnlyOne()) {
                var project = projects.Single();

                var sourceFolderName = sourceFolder.Name.ToUpperFirst();
                var targetFolderName = Collection.GetFolder(target).Name.ToUpperFirst();
                var projectName = Path.GetFileNameWithoutExtension(project.Name);
                if (projectName.Contains(sourceFolderName)) {

                    // rename the project by replacing the source folder name with the target folder name
                    var branchedProject = Collection.GetItem(new Uri(new Uri(target), new Uri(source).MakeRelativePath(project.Path)).LocalPath);
                    var targetName = branchedProject.Name.Replace(sourceFolderName, targetFolderName);
                    var renamedProject = (TfsItem)branchedProject.Rename(targetName);

                    // for C# projects also rename the assembly and root namespace
                    var csProject = renamedProject as TfsCsProject;
                    if (csProject != null) {

                        // rename assembly
                        csProject.AssemblyName = csProject.AssemblyName.Replace(sourceFolderName, targetFolderName);

                        // rename root namespace
                        csProject.RootNamespace = csProject.RootNamespace.Replace(sourceFolderName, targetFolderName);
                    }
                }
            }

            Collection.WriteChanges();
        }
    }

    /// <summary>
    /// Verify project references are valid and projects guids are unique.
    /// </summary>
    [CommandGroup]
    public class Verify : TfsCommand {

        /// <summary/>
        /// <param name="currentDirectory"></param>
        /// <param name="graph">Verify references to projects from projects and solutions are valid. Verify existance of 
        /// expected properties of all solutions and projects as well.</param>
        /// <param name="guids">Enforce that projects have unique GUIDs. The most common cause of duplicate GUIDs is creating a 
        /// new project by copying an existing one without assigning it a new project GUID.</param>
        [Command]
        public Verify(string currentDirectory, bool graph = false, bool guids = false)
            : base(currentDirectory, graph, guids) {
        }

        private IEnumerable Execute(string currentDirectory, bool graph, bool guids) {
            var pass = true;

            if (graph) {
                Console.WriteLine("Verifying Graph");
                CurrentFolder.VerifyProjectGraph();
            }

            if (graph) {
                Console.WriteLine("Verifying Guids");
                var items = CurrentFolder.Get<TfsProject>();
                var projects = items.GroupBy(o => o.Guid).ToArray();
                var duplicates = projects.Where(o => o.MoreThanOne() && o.Key != null);

                if (duplicates.Any()) {
                    pass = false;
                    yield return new TextHeader(
                        "The following projects have duplicate guids") {
                        from o in duplicates
                        select new TextHeader(o.Key.ToString()) {
                            o.Select(x => x.Path)
                        }
                    };
                }
            }

            if (!pass)
                throw new ArgumentException();
        }
    }

    /// <summary>
    /// Enforce solution and project policy for ToolVersion, SolutionVersion, and build events, etc.
    /// </summary>
    [CommandGroup]
    public class Enforce : TfsCommand {

        /// <summary/>
        /// <param name="itemspec">A directory inside the lab.</param>
        /// <param name="toolsVersion">Enforce that projects use the same version of msbuild and associated tools. </param>
        /// <param name="solutionVersion">Enforce that projects use the same version of Visual Studio. Must be 2010 or 2008. </param>
        /// <param name="buildEvents">Disallow pre/post build events during msbuilds. This prevents introduction of hidden dependencies.
        /// Instead, only allow pre/post build events to execute when projects are built insdie Visual Studio.
        /// Do this by requiring those events be wrapped in IF "$(BuildingInsideVisualStudio)" == "true" (...).</param>
        /// <param name="noNestedProjects">Enforce projects do not live in a sub-directory of another project.</param>
        /// <param name="targetFrameworkVersion"></param>
        /// <param name="fix">Wrap bad postbuild events in IF "$(BuildingInsideVisualStudio)" == "true" (...).</param>
        [Command]
        public Enforce(
            [CommandTarget(Path = CommandPathInfo.Exists)]string itemspec, 
            double? toolsVersion = null,
            int? solutionVersion = null,
            bool buildEvents = false,
            bool noNestedProjects = false,
            double? targetFrameworkVersion = null,
            bool fix = false)
            : base(itemspec, toolsVersion, solutionVersion, buildEvents, noNestedProjects, targetFrameworkVersion, fix) {
        }

        private IEnumerable EnforceToolsVersion(double? toolsVersion, bool fix) {
            Console.WriteLine("Enforce Tools Version");

            var projectsWithToolsVersion =
                from o in TfsLoader.Infos()
                where o.Type.GetProperty("ToolsVersion") != null
                select o.Type;

            var projects = CurrentFolder.Get<TfsProject>(projectsWithToolsVersion);

            var badProjects =
               (from o in projects
                let property = o.GetType().GetProperty("ToolsVersion")
                let toolVersions = (double)property.GetValue(o)
                where toolVersions != toolsVersion
                orderby o.Path
                select new { ToolVersions = toolVersions, Property = property, Project = o }).ToArray();

            if (badProjects.Any()) {
                yield return new TextHeader(
                    "The following projects have a ToolsVersion other than " + toolsVersion) {
                    from o in badProjects
                    select new TextJoin(" ") { o.Project.Path, o.ToolVersions.ToString() }
                };
            }

            if (fix) {
                foreach (var o in badProjects)
                    o.Property.SetValue(o.Project, toolsVersion);
            }
        }
        private IEnumerable EnforceSolutionVersion(int? solutionVersion) {
            Console.WriteLine("Enforce Solution Version");

            var solutions = CurrentFolder.Get<TfsSolution>();

            var badSolutions =
               (from o in solutions
                where o.Year != solutionVersion
                orderby o.Path
                select o).ToArray();

            if (badSolutions.Any()) {
                yield return new TextHeader(
                    "The following solutions have a version other than " + solutionVersion) {
                    from o in badSolutions
                    select new TextJoin(" ") { o.Path, o.Year.ToString() }
                };
            }
        }
        private IEnumerable EnforceBuildEvents() {
            Console.WriteLine("Enforce Build Events");

            var csProjects = CurrentFolder.Get<TfsCsProject>();

            foreach (var csProject in csProjects) {
                var newPreBuildEvent = EnforceRunOnlyInVisualStudio(csProject, csProject.PreBuildEvent);
                if (newPreBuildEvent != null)
                    csProject.PreBuildEvent = newPreBuildEvent;

                var oldPreBuildEvent = EnforceRunOnlyInVisualStudio(csProject, csProject.PostBuildEvent);
                if (oldPreBuildEvent != null)
                    csProject.PreBuildEvent = oldPreBuildEvent;
            }

            if (csProjects.Any(o => o.IsDirty)) {
                yield return new TextHeader(
                    "The following C# projects have (pre|post)BuildSteps steps that fail the regex: " +
                    "^IF \"[$][(]BuildingInsideVisualStudio[)]\" == \"true\" (.*)$") {
                    };
            }
        }
        private string EnforceRunOnlyInVisualStudio(object target, string buildEvent) {
            if (string.IsNullOrEmpty(buildEvent))
                return buildEvent;

            var options = RegexOptions.Singleline | RegexOptions.IgnoreCase;
            var result = Regex.IsMatch(buildEvent, "^IF \"[$][(]BuildingInsideVisualStudio[)]\" == \"true\" (.*)$", options);

            if (!result)
                return "IF \"$(BuildingInsideVisualStudio)\" == \"true\" (" + Environment.NewLine +
                    buildEvent + Environment.NewLine +
                ")";

            return null;
        }
        private void EnforceTargetFrameworkVersion(double? targetFrameworkVersion, ref bool pass) {
            throw new NotImplementedException();
            //var projects =
            //    from o in CurrentFolder.Get<TfsCsProject>()
            //    where o.TargetFrameworkVersion != targetFrameworkVersion
            //    select o;

            //foreach (var o in projects)
            //    o.TargetFrameworkVersion = (double)targetFrameworkVersion;

            //var projs = LabFolder.Get<TfsWdProject>();
            //foreach (var proj in projs) {
            //    var xml = proj.Xml.Descendants(TfsCsProject.XNamespace + "TargetFrameworkVersion").SingleOrDefault();
            //    if (xml == null)
            //        continue;
            //    xml.Value = "v4.0";
            //}

            //Collection.WriteChanges();

            //if (projects.Any())
            //    pass = false;
        }
        private IEnumerable EnforceNoNestedProjects() {
            var projects = LabFolder.Get<TfsProject>(typeof(TfsCsProject), typeof(TfsVcxProject));

            var nestedProjects =
               (from project in projects
                from parent in projects
                where project.Url.IsSubUrl(parent.Url)
                select new { project, parent }).ToArray();

            if (nestedProjects.Any()) {
                yield return new TextHeader("The following projects live in a sub-directory of another project:") {
                    from o in nestedProjects
                    select new TextJoin(" => ") { o.project.Path, o.parent.Path }
                };

                throw new ArgumentException();
            }
        }

        private IEnumerable Execute(string itemspec, double? toolsVersion, int? soutionVersion, bool buildEvents,
            bool noNestedProjects, double? targetFrameworkVersion, bool fix) {

            var pass = true;

            if (toolsVersion != null) {
                foreach (var error in EnforceToolsVersion(toolsVersion, fix)) {
                    pass = false;
                    yield return error;
                }
            }

            if (soutionVersion != null) {
                foreach (var error in EnforceSolutionVersion(soutionVersion)) {
                    pass = false;
                    yield return error;
                }
            }

            if (buildEvents) {
                foreach (var error in EnforceBuildEvents()) {
                    pass = false;
                    yield return error;
                }
            }

            if (noNestedProjects)
                yield return EnforceNoNestedProjects();

            if (targetFrameworkVersion != null)
                EnforceTargetFrameworkVersion(targetFrameworkVersion, ref pass);

            if (!pass && !fix)
                throw new ArgumentException("Test failed.");

            if (fix)
                Collection.WriteChanges();
        }        
    }

    /// <summary>
    /// Enforce a set of configurations across all projects in the lab. Detect and strip all other configurations.
    /// </summary>
    [CommandGroup]
    public class Configuration : TfsCommand {
        
        /// <summary/>
        /// <param name="itemspec"></param>
        /// <param name="flavors">A list of required configurations.</param>
        /// <param name="platforms">A list of required platforms.</param>
        /// <param name="include">A project name filter. (e.g. *.csproj,*.wdproj)</param>
        /// <param name="missing">Filter for projects with missing configurations.</param>
        /// <param name="extra">Filter for projects with extra configurations.</param>
        /// <param name="exisiting">Display existing configurations.</param>
        /// <param name="default">Display bad default configurations. The first flavor and 
        /// platform is the required default configuration.</param>
        /// <param name="verbose">Display the missing or extra configurations.</param>
        /// <param name="fix">If /default then set default project configurations to the 
        /// required project configurations. If /extra then strip extra 
        /// project configuration types.</param>
        [Command]
        public Configuration(
            [CommandTarget(Path = CommandPathInfo.Exists)]
            string itemspec = ".", 

            TfsFlavor[] flavors = null,
            TfsPlatform[] platforms = null,
            string[] include = null,
            bool missing = false,
            bool extra = false,
            bool existing = false,
            bool @default = false,
            bool verbose = false,
            bool fix = false)
            : base(itemspec, flavors, platforms, include, missing, extra, existing, @default, verbose, fix) {
        }

        private void DoDefault(TfsFlavor[] flavors, TfsPlatform[] platforms, bool fix,
            ITfsConfigurable configurable, List<string> verbose) {
            var requiredDefaultConfiguration =
                TfsConfiguration.Get(flavors.FirstOrDefault(), platforms.FirstOrDefault());

            var defaultConfiguration = configurable.DefaultConfiguration;
            if (defaultConfiguration != null) {
                if (defaultConfiguration != requiredDefaultConfiguration) {
                    verbose.Add("    default: +{0}".Substitute(defaultConfiguration));
                    if (fix) {
                        if (configurable.Configurations().Contains(requiredDefaultConfiguration)) {
                            configurable.DefaultConfiguration = requiredDefaultConfiguration;
                            verbose.Add("        default fixed.");
                        } else {
                            verbose.Add("        default not fixed because project does not contatin required default configuration.");
                        }
                    }
                }
            }
        }
        private void DoConfigurable(TfsFlavor[] flavors, TfsPlatform[] platforms,
            bool @default, bool missing, bool extra, bool existing, bool fix,
            HashSet<TfsConfiguration> requiredConfigurations, 
            ITfsConfigurable configurable, 
            List<string> verbose) {

            if (@default)
                DoDefault(flavors, platforms, fix, configurable, verbose);

            List<TfsConfiguration> missingCfgs = new List<TfsConfiguration>();
            List<TfsConfiguration> extraCfgs = new List<TfsConfiguration>();
            foreach (var configuration in configurable.Configurations().Union(requiredConfigurations).OrderBy(x => x.ToString())) {
                var isExtra = !requiredConfigurations.Contains(configuration);
                var isMissing = !configurable.Configurations().Contains(configuration);
                var isExisting = configurable.Configurations().Contains(configuration);

                if ((missing && isMissing) || (extra && isExtra) || (existing && isExisting) || (!existing && !missing && !extra))
                    verbose.Add("    {0}{1}{2}".Substitute(isExtra ? "+" : null, isMissing ? "-" : null, configuration));

                if (isMissing) missingCfgs.Add(configuration);
                if (isExtra) extraCfgs.Add(configuration);
            }

            if (fix && extraCfgs.Any()) {
                if (configurable is TfsSolution) {
                    if (extraCfgs.Count() == configurable.Configurations().Count()) {
                        Console.WriteLine("Cannot remove extra configurations from " + Environment.NewLine +
                            ((TfsPath)configurable).Path + " because there would be no configurations left in the solution.");
                        return;
                    }

                } else {
                    if (missingCfgs.Any()) {
                        Console.WriteLine("Cannot remove extra configurations from " + Environment.NewLine +
                            ((TfsPath)configurable).Path + " because the following required configurations are missing: " +
                            missingCfgs.StringJoin(", ", o => o.ToString()));
                        return;
                    }
                }

                foreach (var configuration in extraCfgs) {
                    configurable.RemoveConfiguration(configuration);
                    Console.WriteLine("    Removing " + configuration);
                }
            }
        }

        private IEnumerable Execute(string itemspec, TfsFlavor[] flavors, TfsPlatform[] platforms, string[] include,
            bool missing, bool extra, bool existing, bool @default, bool verbose, bool fix) {
            LabFolder.LoadProjectGraph();

            var requiredConfigurations =
               (from flavor in flavors
                from platform in platforms
                select TfsConfiguration.Get(flavor, platform))
                .ToHashSet();

            var configurables =
               (from configurable in CurrentFolder.Get<ITfsConfigurable>()
                let item = (TfsItem)configurable
                where IncludeItem(item.Name, include)
                orderby item.Path
                select configurable).ToList();

            foreach (var configurable in configurables) {

                var results = new List<string>();

                DoConfigurable(flavors, platforms,
                    @default, missing, extra, existing, fix,
                    requiredConfigurations, configurable, results);

                if (!results.Any())
                    continue;

                var item = (TfsItem)configurable;
                Console.WriteLine(item.Path);

                if (verbose) {
                    foreach (var c in results)
                        Console.WriteLine(c);

                    Console.WriteLine();
                }
            }

            if (fix)
                Collection.WriteChanges();

            return null;
        }

        private bool IncludeItem(string name, string[] include) {
            // compose regex for testing solution and project names for exclusion
            var includeRegex = new Regex(include.Select(o => "^" + o.Replace("*", ".*") + "$").StringJoin("|") ?? ".");

            return includeRegex.IsMatch(name);
        }
    }

    /// <summary>
    /// Deletes all files not already in or pending additions to source control.
    /// </summary>
    //[Command]
    public class Scorch : TfsCommand {

        public void Execute() {
            var paths = CurrentFolder.Paths().ToList();

            var folders = paths.WhereIs<TfsFolder>().Select(o => o.Path);
            var directories = Directory.GetDirectories(CurrentFolder.Path, "*", SearchOption.AllDirectories).Select(o => o.NormalizeDirectory());
            var extraDirectories = directories.Except(folders, StringComparer.InvariantCultureIgnoreCase);
            extraDirectories = extraDirectories.RootDirectories().ToList();

            var files = Directory.GetFiles(CurrentFolder.Path, "*", SearchOption.AllDirectories).ExceptDirs(extraDirectories);
            var items = paths.WhereIs<TfsItem>().Select(o => o.Path);
            var extraFiles = files.Except(items, StringComparer.InvariantCultureIgnoreCase).ToList();

            foreach (var extraDirectory in extraDirectories) {
                foreach (var extraFile in Directory.GetFiles(extraDirectory, "*", SearchOption.AllDirectories)) {
                    Console.WriteLine(extraFile);
                    if (Preview)
                        continue;

                    DeleteFile(extraFile);
                }
            }

            foreach (var extraFile in extraFiles) {
                Console.WriteLine(extraFile);
                if (Preview)
                    continue;

                DeleteFile(extraFile);
            }

            foreach (var extraDirectory in extraDirectories) {

                foreach (var subDirectory in Directory.GetDirectories(extraDirectory, "*", SearchOption.AllDirectories).OrderByDescending(o => o.Length)) {
                    if (!Preview)
                        Directory.Delete(subDirectory);
                }

                Console.WriteLine(extraDirectory);
                if (Preview)
                    continue;

                Directory.Delete(extraDirectory);
            }
        }

        private void DeleteFile(string path) {
            File.SetAttributes(path, File.GetAttributes(path) & ~FileAttributes.ReadOnly);
            File.Delete(path);
        }

        [CommandSwitch]
        public bool Preview { get; set; }
    }
}
